package com.lsl.rdf.utils.bean;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Supplier;

/**
 * Created by lsl on 2021/6/1.
 */
public class IBeanUtil {

    /**
     * 拷贝列表属性，字段类型要一致
     *
     * @param source 被拷贝原数据
     * @param target 拷贝目标类
     * @param <S>    被拷贝对象类型
     * @param <T>    拷贝目标对象类型
     * @return list<T>
     */
    public static <S, T> List<T> copyListProperties(List<S> source, Supplier<T> target) {
        if (CollectionUtils.isEmpty(source)) {
            return Collections.emptyList();
        }
        if (Objects.isNull(target)) {
            return Collections.emptyList();
        }
        return copyListProperties(source, target, null);
    }

    /**
     * 拷贝列表属性，可回调，字段类型要一致
     *
     * @param source   被拷贝原数据
     * @param target   拷贝目标类
     * @param <S>      被拷贝对象类型
     * @param <T>      拷贝目标对象类型
     * @param callback 回调函数
     * @return list<T>
     */
    public static <S, T> List<T> copyListProperties(List<S> source, Supplier<T> target, IBeanUtilCallback<S, T> callback) {
        if (CollectionUtils.isEmpty(source)) {
            return Collections.emptyList();
        }
        if (Objects.isNull(target)) {
            return Collections.emptyList();
        }
        List<T> list = new ArrayList<>(source.size());
        for (S src : source) {
            T t = copyInstance(src, target);
            if (Objects.nonNull(callback)) {
                callback.callBack(src, t);
            }
            list.add(t);
        }
        return list;
    }

    /**
     * 拷贝并创建实例
     *
     * @param source 被拷贝对象
     * @param target 拷贝对象
     * @param <T>    拷贝对象类型
     * @return T
     */
    public static <T> T copyInstance(Object source, Supplier<T> target) {
        T t = target.get();
        copyProperties(source, t, (String[]) null);
        return t;
    }

    /**
     * 拷贝并创建实例
     *
     * @param source           被拷贝对象
     * @param target           拷贝对象
     * @param ignoreProperties 拷贝忽略属性
     * @param <T>              拷贝对象类型
     * @return T
     */
    public static <T> T copyInstance(Object source, Supplier<T> target, String... ignoreProperties) {
        T t = target.get();
        copyProperties(source, t, ignoreProperties);
        return t;
    }

    /**
     * 将给定源bean的属性值复制到给定目标bean中。
     * 注意:只要属性匹配，源类和目标类不必匹配，甚至不必彼此派生。源bean公开但目标bean没有公开的任何bean属性都将被静默地忽略。
     * 从Spring Framework 5.3开始，当匹配源对象和目标对象中的属性时，该方法支持泛型类型信息。
     * <p>此方法是在BeanUtils的方法基础上改造</p>
     *
     * @param source           the source bean
     * @param target           the target bean
     * @param ignoreProperties array of property names to ignore
     * @throws BeansException if the copying failed
     * @see BeanWrapper
     */
    private static void copyProperties(Object source, Object target, @Nullable String... ignoreProperties) throws BeansException {

        Assert.notNull(source, "IBeanUtil.copyProperties() source 不能为空");
        Assert.notNull(target, "IBeanUtil.copyProperties() target 不能为空");

        Class<?> actualEditable = target.getClass();
        PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(actualEditable);
        List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null) {
                        ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
                        ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);

                        // Ignore generic types in assignable check if either ResolvableType has unresolvable generics.
                        boolean isAssignable =
                                (sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ?
                                        ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) :
                                        targetResolvableType.isAssignableFrom(sourceResolvableType));

                        // 判断目标对象字段类型是不是 String 类型
                        boolean isString = targetResolvableType.getType().getTypeName().equals("java.lang.String");

                        if (isAssignable || isString) {
                            try {
                                if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                    readMethod.setAccessible(true);
                                }
                                Object value = readMethod.invoke(source);
                                if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                    writeMethod.setAccessible(true);
                                }
                                if (isString) {
                                    writeMethod.invoke(target, Objects.isNull(value) ? null : value.toString());
                                } else {
                                    writeMethod.invoke(target, value);
                                }
                            } catch (Throwable ex) {
                                throw new FatalBeanException(
                                        "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                            }
                        }
                    }
                }
            }
        }
    }
}
